package com.mike.validators;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLEncoder;
import java.nio.file.Files;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Observable;
import java.util.Random;
import java.util.zip.GZIPInputStream;
import java.util.zip.InflaterInputStream;

/**
 * <p>
 * Title: MyJavaTools: Client HTTP Request class
 * </p>
 * <p>
 * Description: this class helps to send POST HTTP requests with various form
 * data, including files. Cookies can be added to be included in the request.
 * </p>
 * Licensed under the myjavatools license (Apache license version 2.0)
 * http://www.myjavatools.com/license.txt
 * @author Vlad Patryshev
 * @author James Peltzer
 * @version 6.0
 */
class ClientHttpRequest
	extends Observable {
	HttpURLConnection	connection;
	OutputStream		os			= null;
	Map<String, String>	cookies		= new HashMap<String, String>();
	String				rawCookies	= "";

	protected void connect() throws IOException {
		if (os == null)
			os = connection.getOutputStream();
	}

	protected void write(char c) throws IOException {
		connect();
		os.write(c);
	}

	protected void write(String s) throws IOException {
		connect();
		os.write(s.getBytes());
	}

	protected long newlineNumBytes() {
		return 2;
	}

	protected void newline() throws IOException {
		connect();
		write("\r\n");
	}

	protected void writeln(String s) throws IOException {
		connect();
		write(s);
		newline();
	}

	private static Random	random	= new Random();

	protected static String randomString() {
		return Long.toString(random.nextLong(), 36);
	}

	private long boundaryNumBytes() {
		return boundary.length() + 2 /*--*/;
	}

	String	boundary	= "---------------------------" + randomString() + randomString() + randomString();

	private void boundary() throws IOException {
		write("--");
		write(boundary);
	}

	/**
	 * Creates a new multipart POST HTTP request on a freshly opened
	 * URLConnection
	 * @param connection
	 *        an already open URL connection
	 * @throws IOException
	 */
	public ClientHttpRequest(URLConnection connection) throws IOException {
		this.connection = (HttpURLConnection) connection;
		connection.setDoOutput(true);
		connection.setDoInput(true);
		connection.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + boundary);
	}

	/**
	 * Creates a new multipart POST HTTP request for a specified URL
	 * @param url
	 *        the URL to send request to
	 * @throws IOException
	 */
	public ClientHttpRequest(URL url) throws IOException {
		this(url.openConnection());
	}

	/**
	 * Creates a new multipart POST HTTP request for a specified URL string
	 * @param urlString
	 *        the string representation of the URL to send request to
	 * @throws IOException
	 */
	public ClientHttpRequest(String urlString) throws IOException {
		this(new URL(urlString));
	}

	private void postCookies() {
		StringBuffer cookieList = new StringBuffer(rawCookies);
		for (Map.Entry<String, String> cookie : cookies.entrySet()) {
			if (cookieList.length() > 0) {
				cookieList.append("; ");
			}
			cookieList.append(cookie.getKey() + "=" + cookie.getValue());
		}
		if (cookieList.length() > 0) {
			connection.setRequestProperty("Cookie", cookieList.toString());
		}
	}

	/**
	 * Adds a cookie to the requst
	 * @param name
	 *        cookie name
	 * @param value
	 *        cookie value
	 * @throws IOException
	 */
	public void setCookies(String rawCookies) throws IOException {
		this.rawCookies = (rawCookies == null) ? "" : rawCookies;
		cookies.clear();
	}

	/**
	 * Adds a cookie to the requst
	 * @param name
	 *        cookie name
	 * @param value
	 *        cookie value
	 * @throws IOException
	 */
	public void setCookie(String name, String value) throws IOException {
		cookies.put(name, value);
	}

	/**
	 * Adds cookies to the request
	 * @param cookies
	 *        the cookie "name-to-value" map
	 * @throws IOException
	 */
	public void setCookies(Map<String, String> cookies) throws IOException {
		if (cookies != null) {
			this.cookies.putAll(cookies);
		}
	}

	/**
	 * Adds cookies to the request
	 * @param cookies
	 *        array of cookie names and values (cookies[2*i] is a name,
	 *        cookies[2*i + 1] is a value)
	 * @throws IOException
	 */
	public void setCookies(String[] cookies) throws IOException {
		if (cookies != null) {
			for (int i = 0; i < cookies.length - 1; i += 2) {
				setCookie(cookies[i], cookies[i + 1]);
			}
		}
	}
	
	public void setHeader(String key, String value){
		this.connection.setRequestProperty(key, value);
	}

	private long writeNameNumBytes(String name) {
		return newlineNumBytes() + "Content-Disposition: form-data; name=\"".length() + name.getBytes().length + 1 /* '"' */;
	}

	private void writeName(String name) throws IOException {
		newline();
		write("Content-Disposition: form-data; name=\"");
		write(name);
		write('"');
	}

	private boolean	isCanceled	= false;
	private int		bytesSent	= 0;

	public int getBytesSent() {
		return bytesSent;
	}

	public void cancel() {
		isCanceled = true;
	}

	private synchronized void pipe(InputStream in, OutputStream out) throws IOException {
		byte[] buf = new byte[1024];
		int nread;
		bytesSent = 0;
		isCanceled = false;
		synchronized (in) {
			while ((nread = in.read(buf, 0, buf.length)) >= 0) {
				out.write(buf, 0, nread);
				bytesSent += nread;
				if (isCanceled) {
					throw new IOException("Canceled");
				}
				out.flush();
				this.setChanged();
				this.notifyObservers(bytesSent);
				this.clearChanged();
			}
		}
		out.flush();
		buf = null;
	}

	/**
	 * Adds a string parameter to the request
	 * @param name
	 *        parameter name
	 * @param value
	 *        parameter value
	 * @throws IOException
	 */
	public void setParameter(String name, String value) throws IOException {
		boundary();
		writeName(name);
		newline();
		newline();
		writeln(value);
	}

	/**
	 * Adds a file parameter to the request
	 * @param name
	 *        parameter name
	 * @param filename
	 *        the name of the file
	 * @param is
	 *        input stream to read the contents of the file from
	 * @throws IOException
	 */
	public void setParameter(String name, String filename, String contentType, InputStream is) throws IOException {
		boundary();
		writeName(name);
		write("; filename=\"");
		write(filename);
		write('"');
		newline();
		write("Content-Type: ");
		writeln(contentType);
		newline();
		pipe(is, os);
		newline();
	}

	public long getFilePostSize(String name, File file) {
		String filename = file.getPath();
		String type = URLConnection.guessContentTypeFromName(filename);
		if (type == null)
			type = "application/octet-stream";

		return boundaryNumBytes() + writeNameNumBytes(name) + "; filename=\"".length() + filename.getBytes().length + 1 + newlineNumBytes() + "Content-Type: ".length()
				+ type.length() + newlineNumBytes() + newlineNumBytes() + file.length() + newlineNumBytes();
	}

	/**
	 * Adds a file parameter to the request
	 * @param name
	 *        parameter name
	 * @param file
	 *        the file to upload
	 * @throws IOException
	 */
	public void setParameter(String name, File file) throws IOException {
		FileInputStream fis = null;
		try {
			fis = new FileInputStream(file);
			String type = Files.probeContentType(file.toPath());
			if (type == null)
				type = "application/octet-stream";
			setParameter(name, file.getName(), type, fis);
		} finally {
			if (fis != null) {
				fis.close();
			}
		}
	}

	/**
	 * Adds a parameter to the request; if the parameter is a File, the file is
	 * uploaded, otherwise the string value of the parameter is passed in the
	 * request
	 * @param name
	 *        parameter name
	 * @param object
	 *        parameter value, a File or anything else that can be stringified
	 * @throws IOException
	 */
	public void setParameter(Object name, Object object) throws IOException {
		if (object instanceof File) {
			setParameter(name.toString(), (File) object);
		} else {
			setParameter(name.toString(), object.toString());
		}
	}

	/**
	 * Adds parameters to the request
	 * @param parameters
	 *        "name-to-value" map of parameters; if a value is a file, the file
	 *        is uploaded, otherwise it is stringified and sent in the request
	 * @throws IOException
	 */
	public void setParameters(Map<Object, Object> parameters) throws IOException {
		if (parameters != null) {
			for (Iterator<Entry<Object, Object>> i = parameters.entrySet().iterator(); i.hasNext();) {
				Map.Entry<Object, Object> entry = (Map.Entry<Object, Object>) i.next();
				if (entry.getValue() instanceof String)
					setParameter(entry.getKey().toString(), URLEncoder.encode((String) entry.getValue(), "UTF-8"));
				else
					setParameter(entry.getKey().toString(), entry.getValue());
			}
		}
	}

	/**
	 * Adds parameters to the request
	 * @param parameters
	 *        (vararg) parameter names and values (parameters[2*i] is a name,
	 *        parameters[2*i + 1] is a value); if a value is a file, the file is
	 *        uploaded, otherwise it is stringified and sent in the request
	 * @throws IOException
	 */
	public void setParameters(Object... parameters) throws IOException {
		for (int i = 0; i < parameters.length - 1; i += 2) {
			setParameter(parameters[i].toString(), parameters[i + 1]);
		}
	}

	public long getPostFooterSize() {
		return boundaryNumBytes() + 2 /*--*/+ newlineNumBytes() + newlineNumBytes();
	}
	
	public boolean isDeflated(){
		String encoding = connection.getContentEncoding();
		return "deflate".equals(encoding);
	}
	
	public boolean isGzipped(){
		String encoding = connection.getContentEncoding();
		return "gzip".equals(encoding);
	}
	
	/**
	 * <p>Sets a specified timeout value, in milliseconds, to be used when opening a communications 
	 * link to the resource referenced by this URLConnection. If the timeout expires before the connection 
	 * can be established, a java.net.SocketTimeoutException is raised. A timeout of zero is 
	 * interpreted as an infinite timeout.</p>
	 * <p> 
	 * Some non-standard implmentation of this method may ignore the specified timeout. 
	 * To see the connect timeout set, please call getConnectTimeout().
	 * </p> 
	 * @param timeout
	 * @throws IllegalArgumentException if the timout parameter is negative.
	 */
	public void setConnectionTimeout(int timeout) throws IllegalArgumentException{
		connection.setConnectTimeout(timeout);
	}
	
	/**
	 * Returns setting for connect timeout. 
	 * 0 return implies that the option is disabled (i.e., timeout of infinity). 
	 * @return
	 */
	public int getConnectionTimeout(){
		return connection.getConnectTimeout();
	}
	
	public void setReadTimeout(int timeout) throws IllegalArgumentException {
		connection.setReadTimeout(timeout);
	}
	
	public int getReadTimeout(){
		return connection.getReadTimeout();
	}

	/**
	 * Posts the requests to the server, with all the cookies and parameters
	 * that were added
	 * @return input stream with the server response
	 * @throws IOException
	 */
	private InputStream doPost() throws ServerResponseException {
		try {
			boundary();
			writeln("--");
			os.close();
			if(isDeflated())
				return new InflaterInputStream(connection.getInputStream());
			else if(isGzipped())
				return new GZIPInputStream(connection.getInputStream());
			else
				return connection.getInputStream();
		} catch (IOException e1) {
			String message = "";
			int statusCode = -1;
			try {
				statusCode = connection.getResponseCode();
				BufferedReader error;
				if (connection != null && connection.getErrorStream() != null) {
					error = new BufferedReader(new InputStreamReader(connection.getErrorStream()));
					String str;
					while (null != ((str = error.readLine())))
						message += str + "\n";
				}
			} catch (IOException e2) {
				if(statusCode == -1)
					throw new ServerResponseException("I couldn't get a proper status code and I could not get the error message!  Sorry!\n" + e2.getMessage(), statusCode);
				else
					throw new ServerResponseException("The server returned code "+statusCode+", and I could not get the error message.  Sorry!\n" + e2.getMessage(), statusCode);
			}
			
			if(message.isEmpty())
				message = "The request could not be completed";
			throw new ServerResponseException(message, statusCode);
		}
	}

	/**
	 * Posts the requests to the server, with all the cookies and parameters
	 * that were added
	 * @return input stream with the server response
	 * @throws ServerResponseException
	 */
	public InputStream post() throws ServerResponseException {
		postCookies();
		return doPost();
	}

	/**
	 * Posts the requests to the server, with all the cookies and parameters
	 * that were added before (if any), and with parameters that are passed in
	 * the argument
	 * @param parameters
	 *        request parameters
	 * @return input stream with the server response
	 * @throws IOException
	 * @throws ServerResponseException
	 * @see setParameters
	 */
	public InputStream post(Map<Object, Object> parameters) throws ServerResponseException, IOException {
		postCookies();
		setParameters(parameters);
		return doPost();
	}

	/**
	 * Posts the requests to the server, with all the cookies and parameters
	 * that were added before (if any), and with parameters that are passed in
	 * the argument
	 * @param parameters
	 *        request parameters
	 * @return input stream with the server response
	 * @throws IOException
	 * @throws ServerResponseException
	 * @see setParameters
	 */
	public InputStream post(Object... parameters) throws ServerResponseException, IOException {
		postCookies();
		setParameters(parameters);
		return doPost();
	}

	/**
	 * Posts the requests to the server, with all the cookies and parameters
	 * that were added before (if any), and with cookies and parameters that are
	 * passed in the arguments
	 * @param cookies
	 *        request cookies
	 * @param parameters
	 *        request parameters
	 * @return input stream with the server response
	 * @throws IOException
	 * @throws ServerResponseException
	 * @see setParameters
	 * @see setCookies
	 */
	public InputStream post(Map<String, String> cookies, Map<Object, Object> parameters) throws ServerResponseException, IOException {
		setCookies(cookies);
		postCookies();
		setParameters(parameters);
		return doPost();
	}

	/**
	 * Posts the requests to the server, with all the cookies and parameters
	 * that were added before (if any), and with cookies and parameters that are
	 * passed in the arguments
	 * @param cookies
	 *        request cookies
	 * @param parameters
	 *        request parameters
	 * @return input stream with the server response
	 * @throws IOException
	 * @throws ServerResponseException
	 * @see setParameters
	 * @see setCookies
	 */
	public InputStream post(String raw_cookies, Map<Object, Object> parameters) throws ServerResponseException, IOException {
		setCookies(raw_cookies);
		postCookies();
		setParameters(parameters);
		return doPost();
	}

	/**
	 * Posts the requests to the server, with all the cookies and parameters
	 * that were added before (if any), and with cookies and parameters that are
	 * passed in the arguments
	 * @param cookies
	 *        request cookies
	 * @param parameters
	 *        request parameters
	 * @return input stream with the server response
	 * @throws IOException
	 * @throws ServerResponseException
	 * @see setParameters
	 * @see setCookies
	 */
	public InputStream post(String[] cookies, Object[] parameters) throws ServerResponseException, IOException {
		setCookies(cookies);
		postCookies();
		setParameters(parameters);
		return doPost();
	}

	/**
	 * Posts a new request to specified URL, with parameters that are passed in
	 * the argument
	 * @param parameters
	 *        request parameters
	 * @return input stream with the server response
	 * @throws IOException
	 * @throws ServerResponseException
	 * @see setParameters
	 */
	public static InputStream post(URL url, Map<Object, Object> parameters) throws ServerResponseException, IOException {
		return new ClientHttpRequest(url).post(parameters);
	}

	/**
	 * Posts a new request to specified URL, with parameters that are passed in
	 * the argument
	 * @param parameters
	 *        request parameters
	 * @return input stream with the server response
	 * @throws IOException
	 * @throws ServerResponseException
	 * @see setParameters
	 */
	public static InputStream post(URL url, Object[] parameters) throws ServerResponseException, IOException {
		return new ClientHttpRequest(url).post(parameters);
	}

	/**
	 * Posts a new request to specified URL, with cookies and parameters that
	 * are passed in the argument
	 * @param cookies
	 *        request cookies
	 * @param parameters
	 *        request parameters
	 * @return input stream with the server response
	 * @throws IOException
	 * @throws ServerResponseException
	 * @see setCookies
	 * @see setParameters
	 */
	public static InputStream post(URL url, Map<String, String> cookies, Map<Object, Object> parameters) throws ServerResponseException, IOException {
		return new ClientHttpRequest(url).post(cookies, parameters);
	}

	/**
	 * Posts a new request to specified URL, with cookies and parameters that
	 * are passed in the argument
	 * @param cookies
	 *        request cookies
	 * @param parameters
	 *        request parameters
	 * @return input stream with the server response
	 * @throws IOException
	 * @throws ServerResponseException
	 * @see setCookies
	 * @see setParameters
	 */
	public static InputStream post(URL url, String[] cookies, Object[] parameters) throws ServerResponseException, IOException {
		return new ClientHttpRequest(url).post(cookies, parameters);
	}
}
